﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Threading;
using DistributedVisionRunner.Interface.Model;
using Prism.Events;

namespace DistributedVisionRunner.Interface.Client
{
    /// <summary>
    /// Use this class when vision runner is a prism module in ALC
    /// </summary>
    public class InProcessVisionClient : IVisionClient
    {
        private readonly IEventAggregator _ea;
        private readonly ConcurrentDictionary<Guid, ManualResetEvent> _resultBlockers = new ConcurrentDictionary<Guid, ManualResetEvent>();

        private readonly ConcurrentDictionary<Guid, DistributedVisionRunnerResultPack> _results = new ConcurrentDictionary<Guid, DistributedVisionRunnerResultPack>();

        private readonly ConcurrentDictionary<Guid, DistributedVisionRunnerResultPack> _timeoutResults = new ConcurrentDictionary<Guid, DistributedVisionRunnerResultPack>();

        private static readonly List<string> AdapterNamesTracker = new List<string>();

        private readonly ManualResetEvent _boundaryItemsBlocker = new ManualResetEvent(false);
        private Dictionary<OutputDataType, List<BoundaryItem>> _boundaryItems;

        public InProcessVisionClient(IEventAggregator ea, string adapterName)
        {
            _ea = ea;

            if (AdapterNamesTracker.Contains(adapterName)) throw new DuplicateNameException($"Can not use the same adapter name({adapterName}) for more than one instance");
            AdapterName = adapterName;
            AdapterNamesTracker.Add(adapterName);

            _ea.GetEvent<VisionResultEvent>().Subscribe(ReceiveResult);
            _ea.GetEvent<BoundaryItemsRespondEvent>().Subscribe(items =>
            {
                if (items.adapterName == AdapterName)
                {
                    if (items.behavior == EventBehavior.Response)
                    {
                        _boundaryItems = items.boudaryItems;
                        _boundaryItemsBlocker.Set();
                    }

                    else if (items.behavior == EventBehavior.ChangeNotification)
                    {
                        _boundaryItems = items.boudaryItems;
                        BoundaryItemsChanged?.Invoke(_boundaryItems);
                    }
                }
            });
        }

        public string AdapterName { get; }

        private void ReceiveResult((DistributedVisionRunnerResultPack pack, Guid id) packAndId)
        {
            var id = packAndId.id;

            if (_timeoutResults.TryRemove(id, out var _)) { }
            else _results.TryAdd(id, packAndId.pack);

            var removeBlockerSuccess = _resultBlockers.TryRemove(id, out var blocker);
            blocker.Set();
        }


        public DistributedVisionRunnerResultPack RequestProcess(string inputSn, string productType, int cavity,
            byte[] data, DateTime? creationTime = null, int? timeout = null)
        {
            var sn = string.IsNullOrEmpty(inputSn) ? string.Empty : inputSn;
            if (cavity < 1) throw new ArgumentException("Cavity can not be less than 1", nameof(cavity));
            if (data == null || !data.Any()) throw new ArgumentException("Data can not be empty or null", nameof(data));
            if (timeout != null && timeout.Value <= 0) throw new ArgumentException("Timeout must be a positive number", nameof(timeout));

            var guid = Guid.NewGuid();

            var resultBlocker = AddBlocker(guid);
            _ea.GetEvent<DataEvent>().Publish((data, productType, cavity, sn, creationTime, guid, AdapterName));

            if (timeout == null)
                resultBlocker.WaitOne();
            else
                resultBlocker.WaitOne(timeout.Value);

            // if in time
            if (_results.TryRemove(guid, out var result)) return result;

            // If timeout 
            _timeoutResults.TryAdd(guid, null);
            throw new TimeoutException($"Process task can not finish within {timeout.Value} ms");
        }

        public Dictionary<OutputDataType, List<BoundaryItem>> GetBoundaryItems(int? timeout = null)
        {
            _boundaryItemsBlocker.Reset();
            _ea.GetEvent<BoundaryItemsRequestEvent>().Publish(AdapterName);
            _boundaryItemsBlocker.WaitOne();

            return _boundaryItems;
        }

        public event Action<Dictionary<OutputDataType, List<BoundaryItem>>> BoundaryItemsChanged;


        /// <summary>
        /// Get a ManualResetEvent that is reset
        /// </summary>
        /// <param name="guid"></param>
        /// <returns></returns>
        private ManualResetEvent AddBlocker(Guid guid)
        {
            var blocker = new ManualResetEvent(false);
            if (_resultBlockers.TryAdd(guid, blocker))
                return blocker;

            throw new InvalidOperationException();
        }
    }
}